package com.vst.orm.search.jpa;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Transient;

import com.vst.orm.search.Metadata;

public class JPAAnnotationMetadata
  implements Metadata
{
  Class<?> klass;
  
  private JPAAnnotationMetadata(Class<?> klass)
  {
    this.klass = klass;
  }
  
  static Map<Class<?>, JPAAnnotationMetadata> metadataCache = new HashMap();
  Map<String, Property> props;
  
  public static <T> Metadata getMetadata(Type type)
  {
    Class<?> klass = null;
    Type[] typeArguments = null;
    if ((type instanceof ParameterizedType))
    {
      typeArguments = ((ParameterizedType)type).getActualTypeArguments();
      type = ((ParameterizedType)type).getRawType();
    }
    if ((type instanceof Class)) {
      klass = (Class)type;
    } else {
      throw new IllegalArgumentException("weirdness");
    }
    if (Collection.class.isAssignableFrom(klass))
    {
      if ((typeArguments == null) || (typeArguments.length != 1)) {
        throw new IllegalArgumentException("weirdness again.");
      }
      if (!(typeArguments[0] instanceof Class)) {
        throw new IllegalArgumentException("weirdness a third time.");
      }
      return new JPAAnnotationCollectionMetadata((Class)typeArguments[0], klass);
    }
    if (klass.isArray()) {
      return new JPAAnnotationCollectionMetadata(klass.getComponentType(), klass);
    }
    JPAAnnotationMetadata md = (JPAAnnotationMetadata)metadataCache.get(klass);
    if (md == null)
    {
      md = new JPAAnnotationMetadata(klass);
      metadataCache.put(klass, md);
    }
    return md;
  }
  
  public Class<?> getCollectionClass()
  {
    return null;
  }
  
  public String getIdProperty()
  {
    for (Property prop : getProps().values()) {
      if ((prop.hasAnnotation(Id.class)) || (prop.hasAnnotation(EmbeddedId.class))) {
        return prop.name;
      }
    }
    return null;
  }
  
  public Metadata getIdType()
  {
    String idProp = getIdProperty();
    if (idProp != null) {
      return getPropertyType(idProp);
    }
    return null;
  }
  
  public Serializable getIdValue(Object object)
  {
    String idProp = getIdProperty();
    if (idProp != null) {
      return (Serializable)getPropertyValue(object, idProp);
    }
    return null;
  }
  
  public Class<?> getJavaClass()
  {
    return this.klass;
  }
  
  public String[] getProperties()
  {
    String[] array = new String[getProps().size()];
    int i = 0;
    for (String prop : getProps().keySet()) {
      array[(i++)] = prop;
    }
    return array;
  }
  
  public Metadata getPropertyType(String property)
  {
    Property prop = (Property)getProps().get(property);
    if (prop == null) {
      return null;
    }
    return getMetadata(prop.getType());
  }
  
  public Object getPropertyValue(Object object, String property)
  {
    Property prop = (Property)getProps().get(property);
    if (prop == null) {
      return null;
    }
    return prop.getValue(object);
  }
  
  public boolean isCollection()
  {
    return false;
  }
  
  public boolean isEmeddable()
  {
    return null != this.klass.getAnnotation(Embeddable.class);
  }
  
  public boolean isEntity()
  {
    return null != this.klass.getAnnotation(Entity.class);
  }
  
  public boolean isNumeric()
  {
    return Number.class.isAssignableFrom(this.klass);
  }
  
  public boolean isString()
  {
    return String.class.equals(this.klass);
  }
  
  public synchronized Map<String, Property> getProps()
  {
    if (this.props != null) {
      return this.props;
    }
    this.props = new TreeMap();
    if ((!isEntity()) && (!isEmeddable())) {
      return this.props;
    }
    for (Field field : this.klass.getFields()) {
      if ((null != field.getDeclaringClass().getAnnotation(Entity.class)) || (null != field.getDeclaringClass().getAnnotation(Embeddable.class))) {
        this.props.put(field.getName(), new Property(field));
      }
    }
    for (Method method : this.klass.getMethods())
    {
      String[] name = getterName(method);
      if ((name != null) && (
        (null != method.getDeclaringClass().getAnnotation(Entity.class)) || (null != method.getDeclaringClass().getAnnotation(Embeddable.class))))
      {
        Property property = (Property)this.props.get(name[0]);
        if (property == null)
        {
          property = new Property(name[0]);
          this.props.put(name[0], property);
        }
        property.getter = method;
        if (property.field == null) {
          try
          {
            property.field = this.klass.getDeclaredField(name[0]);
            if (!property.field.getGenericType().equals(property.getter.getGenericReturnType())) {
              property.field = null;
            }
          }
          catch (SecurityException e) {}catch (NoSuchFieldException e) {}
        }
        try
        {
          property.setter = this.klass.getMethod("set" + name[1], new Class[] { method.getReturnType() });
        }
        catch (SecurityException e) {}catch (NoSuchMethodException e) {}
      }
    }
    for (Iterator<Map.Entry<String, Property>> iterator = this.props.entrySet().iterator(); iterator.hasNext();)
    {
      Property property = (Property)((Map.Entry)iterator.next()).getValue();
      if (property.hasAnnotation(Transient.class)) {
        iterator.remove();
      }
    }
    return this.props;
  }
  
  public static String[] getterName(Method method)
  {
    if ((method.getParameterTypes().length != 0) || (method.getReturnType() == null)) {
      return null;
    }
    String name = method.getName();
    if ((name.length() > 3) && (name.startsWith("get"))) {
      name = name.substring(3);
    } else if ((name.length() > 2) && ((method.getReturnType().equals(Boolean.class)) || (method.getReturnType().equals(Boolean.TYPE))) && (name.startsWith("is"))) {
      name = name.substring(2);
    } else {
      return null;
    }
    if (Character.isUpperCase(name.charAt(0))) {
      return new String[] { name.substring(0, 1).toLowerCase() + name.substring(1), name };
    }
    return null;
  }
  
  private static class Property
  {
    String name;
    Field field;
    Method getter;
    Method setter;
    
    public Property(String name)
    {
      this.name = name;
    }
    
    public Property(Field field)
    {
      this.name = field.getName();
      this.field = field;
    }
    
    public Type getType()
    {
      if (this.getter != null) {
        return this.getter.getGenericReturnType();
      }
      if (this.field != null) {
        return this.field.getGenericType();
      }
      if (this.setter != null) {
        return this.setter.getGenericParameterTypes()[0];
      }
      return null;
    }
    
    public Object getValue(Object o)
    {
      try
      {
        if (this.getter != null) {
          return this.getter.invoke(o, new Object[0]);
        }
        if (this.field != null) {
          return this.field.get(o);
        }
      }
      catch (IllegalArgumentException e)
      {
        throw new RuntimeException("Unexpected error getting value of property");
      }
      catch (IllegalAccessException e)
      {
        throw new RuntimeException("Unexpected error getting value of property");
      }
      catch (InvocationTargetException e)
      {
        throw new RuntimeException("Unexpected error getting value of property");
      }
      return null;
    }
    
    public <T extends Annotation> boolean hasAnnotation(Class<T> annotationClass)
    {
      return getAnnotation(annotationClass) != null;
    }
    
    public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
    {
      T val = null;
      if (this.getter != null) {
        val = this.getter.getAnnotation(annotationClass);
      }
      if ((val == null) && (this.field != null)) {
        val = this.field.getAnnotation(annotationClass);
      }
      if ((val == null) && (this.setter != null)) {
        val = this.setter.getAnnotation(annotationClass);
      }
      return val;
    }
  }
}

